using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Dorm.Helpers;
using Dorm.Maps;

namespace Dorm.QueryObject
{
    public enum Prefix { Normal, Count, Top, Max }

    /// <summary>
    /// Represents a query definition for a specific type.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    public class Query<T>
    {
        #region Properties

        protected Prefix Prefix { get; set; }
        protected int Quantity { get; set; }
        protected string MaxField { get; set; }

        protected string EntityName { get { return typeof(T).Name; } }

        public List<FilterExpression> Filters { get; set; }
        protected string WhereClause { get; set; }

        protected List<OrderByExpression> Orders { get; set; }
        protected string OrderByClause { get; set; }

        public List<JoinExpression> PropertiesToJoin { get; set; }
        public List<String> PropertiesToLoad { get; set; }

        public List<MappedProperty> EntityProperties { get; set; }

        #endregion

        #region Constructors and Prefix

        public Query()
        {
            Prefix = Prefix.Normal;
            EntityProperties = MapLocator.GetMap<T>();
            PropertiesToJoin = CreateJoinExpressions(EntityProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Reference));
        }

        private List<JoinExpression> CreateJoinExpressions(IEnumerable<MappedProperty> joinProperties)
        {
            return joinProperties.Select(joinProperty => new JoinExpression
            {
                ForeignEntityType = joinProperty.PropertyType, 
                PropertyName = joinProperty.PropertyName, 
                JoinType = JoinType.LEFT
            }).ToList();
        }

        /// <summary>
        /// Defines the number of results of the query.
        /// </summary>
        public Query<T> Top(int quantity)
        {
            Prefix = Prefix.Top;
            Quantity = quantity;
            return this;
        }

        /// <summary>
        /// Defines the query should return only the number of records that meet the specified criteria.
        /// </summary>
        public Query<T> Count()
        {
            Prefix = Prefix.Count;
            return this;
        }

        /// <summary>
        /// Defines the query should return only the max value found for the specified property.
        /// </summary>
        public Query<T> Max(Expression<Func<T, object>> property)
        {
            Prefix = Prefix.Max;
            MaxField = LambdaParser.GetFieldName<T>(property);
            return this;
        }

        #endregion      

        #region Load
        /// <summary>
        /// Specifies the collection property that should be loaded for each entity returned with this query.
        /// </summary>
        public Query<T> Load(Expression<Func<T, object>> propertyToLoad)
        {
            string listPropertyName = LambdaParser.GetFieldName<T>(propertyToLoad);

            if (PropertiesToLoad == null)
                PropertiesToLoad = new List<string>();

            if (!PropertiesToLoad.Contains(listPropertyName))
                PropertiesToLoad.Add(listPropertyName);

            return this;
        }

        /// <summary>
        /// Specifies that all collection properties should be loaded for each entity returned with this query.
        /// </summary>
        public Query<T> LoadAll()
        {
            PropertiesToLoad = new List<string>();

            foreach (var mappedProperty in EntityProperties
                  .Where(mappedProperty => mappedProperty.MappingType == MappedProperty.MappingTypes.HasMany 
                      || mappedProperty.MappingType == MappedProperty.MappingTypes.HasManyToMany))
            {
                PropertiesToLoad.Add(mappedProperty.PropertyName);
            }

            return this;
        }
        #endregion

        #region Filter

        /// <summary>
        /// Specify the scalar property to be used on a filter.
        /// (For component or reference properties, use the overload with the string parameter).
        /// </summary>
        public FilterBuilder<T> Filter(Expression<Func<T, object>> property)
        {
            return Filter(LambdaParser.GetFieldName<T>(property));
        }

        /// <summary>
        /// Specify the property (scalar, component or reference) to be used on a filter.
        /// </summary>
        public FilterBuilder<T> Filter(string propertyName)
        {
            // Remove the entity prefix if exists
            if (propertyName.Contains(".") && propertyName.StartsWith(EntityName))
                propertyName = propertyName.Substring(propertyName.IndexOf("."));

            // If it still contains a ".", is a Component or Reference
            // Remove the "." if it's a component property.
            if (propertyName.Contains("."))
            {
                string prefix = propertyName.Substring(0, propertyName.IndexOf("."));
                var propertyMap = EntityProperties.Find(m => m.PropertyName == prefix);

                if (propertyMap.MappingType == MappedProperty.MappingTypes.Component)
                    propertyName = propertyName.Replace(".", "");

                else if (propertyMap.MappingType != MappedProperty.MappingTypes.Reference)
                    propertyName = typeof(T).Name + "." + propertyName;
            }
            else
            {
                // If it's a reference without a field, adds the ID suffix to form the FK name.
                if (propertyName != "ID")
                {
                    var propertyMap = EntityProperties.Find(m => m.PropertyName == propertyName);
                    if (propertyMap.MappingType == MappedProperty.MappingTypes.Reference)
                        propertyName = propertyName + "ID";
                }

                if (!propertyName.StartsWith(EntityName + "."))
                    propertyName = typeof(T).Name + "." + propertyName;
            }

            return new FilterBuilder<T>(this, propertyName);
        }

        /// <summary>
        /// A shortcut for selecting a specific id.
        /// </summary>
        public Query<T> GetID(long id)
        {
            return Where(typeof(T).Name + ".ID = " + id);
        }

        /// <summary>
        /// Option for specifying a direct sql where expression.
        /// </summary>
        public Query<T> Where(string whereClause)
        {
            WhereClause = whereClause;
            return this;
        }

        #endregion

        #region Order

        /// <summary>
        /// Specify the scalar property to be used on a order by expression.
        /// (For component or reference properties, use the overload with the string parameter).
        /// </summary>
        public Query<T> Order(Expression<Func<T, object>> propertyToOrder)
        {
            return Order(propertyToOrder, true);
        }

        /// <summary>
        /// Specify the scalar property to be used on an order by expression.
        /// (For component or reference properties, use the overload with the string parameter).
        /// </summary>
        public Query<T> Order(Expression<Func<T, object>> propertyToOrder, bool ascending)
        {
            string propertyName = LambdaParser.GetFieldName<T>(propertyToOrder);
            return Order(propertyName, ascending);
        }

        /// <summary>
        /// Specify the property (scalar, component or reference) to be used on an order by expression.
        /// </summary>
        public Query<T> Order(string propertyName, bool ascending)
        {
            if (Orders == null) Orders = new List<OrderByExpression>();
            Orders.Add(new OrderByExpression {PropertyName = propertyName, Ascending = ascending});

            return this;
        }

        /// <summary>
        /// Option for specifying a direct sql order by expression.
        /// </summary>
        public Query<T> OrderBy(string orderByClause)
        {
            if (!orderByClause.Contains("."))
                orderByClause = typeof (T).Name + "." + orderByClause;

            OrderByClause = orderByClause;
            return this;
        }

        #endregion
        
        /// <summary>
        /// Generates the SQL statement based on the query definition.
        /// </summary>
        public override string ToString()
        {
            var query = new StringBuilder();

            query.AppendLine("SELECT ");

            if (Prefix == Prefix.Count)
                query.AppendLine("COUNT (*) ");

            else if (Prefix == Prefix.Top)
                query.AppendLine("TOP (" + Quantity + ") ");

            else if (Prefix == Prefix.Max)
                query.AppendLine("MAX (" + MaxField+ ") ");

            if (Prefix != Prefix.Count && Prefix != Prefix.Max)
                AddFields(query);

            AddFrom(query);

            AddJoins(query);

            if (Filters != null)
                AddFilters(query);
            if (WhereClause != null)
                AddWhere(query);           
                        
            if (Prefix != Prefix.Count)
            {
                if (Orders != null)
                    AddOrders(query);
                else if (OrderByClause != null)
                    AddOrderByClause(query);
            }

            return query.ToString();
        }

        private void AddFields(StringBuilder query)
        {
            // ID
            query.AppendLine("  " + EntityName + ".ID" + " " + EntityName + "_ID");
            
            // Scalar Properties
            foreach (var mappedProperty in EntityProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Simple))
            {
                query.AppendLine(", " + EntityName + "." + mappedProperty.PropertyName + " " + EntityName + "_" + mappedProperty.PropertyName);
            }

            // Component Properties 
            foreach (var componentMappedEntity in EntityProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Component))
            {
                var componentProperties = MapLocator.GetMap(componentMappedEntity.PropertyType);

                //Scalar
                foreach(var componentMappedProperty in componentProperties.Where(c => c.MappingType == MappedProperty.MappingTypes.Simple))
                {
                    query.Append(", ");
                    query.Append(EntityName + ".");
                    query.Append(componentMappedEntity.PropertyName);
                    query.Append(componentMappedProperty.PropertyName);
                    query.Append(" " + EntityName + "_");
                    query.Append(componentMappedEntity.PropertyName + "_");
                    query.AppendLine(componentMappedProperty.PropertyName);
                }

                // Join
                var joinProperties = CreateJoinExpressions( componentProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Reference));
                foreach (var join in joinProperties)
                {
                    var joinMappedProperties = MapLocator.GetMap(join.ForeignEntityType);

                    query.AppendLine(", " + componentMappedEntity.PropertyName + "_" + join.PropertyName + ".ID" + " " + componentMappedEntity.PropertyName + "_" + join.PropertyName + "_ID");
                    foreach (var joinMappedProperty in joinMappedProperties)
                    {
                        if (joinMappedProperty.MappingType == MappedProperty.MappingTypes.Simple)
                        {
                            query.Append(", " + componentMappedEntity.PropertyName + "_" + join.PropertyName + "." + joinMappedProperty.PropertyName + " ");
                            query.AppendLine(componentMappedEntity.PropertyName + "_" + join.PropertyName + "_" + joinMappedProperty.PropertyName);
                        }
                    }
                }
            }

            // Join Properties
            foreach (var join in PropertiesToJoin)
            {
                var joinMappedProperties = MapLocator.GetMap(join.ForeignEntityType);

                query.AppendLine(", " + join.PropertyName + ".ID" + " " + join.PropertyName + "_ID");
                foreach (var joinMappedProperty in joinMappedProperties)
                {
                    if (joinMappedProperty.MappingType == MappedProperty.MappingTypes.Simple)
                        query.AppendLine(", " + join.PropertyName + "." + joinMappedProperty.PropertyName + " " + join.PropertyName + "_" + joinMappedProperty.PropertyName);
                }
            }
        }

        private void AddFrom(StringBuilder query)
        {
            query.Append("FROM " + EntityName);

            if (EntityName != EntityName)
                query.Append(" " + EntityName);

            query.AppendLine();
        }

        private void AddJoins(StringBuilder query)
        {
            // Direct References
            if (PropertiesToJoin != null)
            {
                foreach (var join in PropertiesToJoin)
                {
                    query.Append(join.JoinType + " JOIN " + join.ForeignEntityType.Name + " " + join.PropertyName);
                    query.AppendLine();
                    query.AppendLine("ON " + EntityName + "." + join.ForeignKeyName + " = " + join.PropertyName + "." + join.ParentKeyName);
                }
            }

            // Component References
            foreach (var componentMappedEntity in EntityProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Component))
            {
                var componentProperties = MapLocator.GetMap(componentMappedEntity.PropertyType);
                var joinProperties = CreateJoinExpressions( componentProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Reference));
                foreach (var join in joinProperties)
                {
                    query.Append(join.JoinType + " JOIN " + join.ForeignEntityType.Name + " " + componentMappedEntity.PropertyName + "_" + join.PropertyName);
                    query.AppendLine();
                    query.AppendLine("ON " + EntityName + "." + componentMappedEntity.PropertyName +join.ForeignKeyName + " = " + componentMappedEntity.PropertyName + "_" + join.PropertyName + "." + join.ParentKeyName);
                }
            }
        }

        private void AddFilters(StringBuilder query)
        {
            query.AppendLine("WHERE");

            query.Append("      ");

            var directProperties = from property in EntityProperties
                                   where property.MappingType != MappedProperty.MappingTypes.HasMany
                                     && property.MappingType != MappedProperty.MappingTypes.HasManyToMany
                                   select property;
            
            var directFilters = (from filter in Filters
                                 where directProperties.Any(p => filter.PropertyName.Contains(p.PropertyName))
                                 || filter.PropertyName == EntityName + ".ID"
                                 select filter).ToList();

            var childrenFilters = (from filter in Filters
                                   where !directProperties.Any(p => filter.PropertyName.Contains(p.PropertyName))
                                   && filter.PropertyName != EntityName + ".ID"
                                   select filter).ToList();

            
            AddDirectFilters(query, directFilters);

            AddChildFilters(query, directFilters, childrenFilters);
        }

        private void AddChildFilters(StringBuilder query, List<FilterExpression> directFilters, List<FilterExpression> childrenFilters)
        {
            int cont = 0;
            foreach(var childFilter in childrenFilters)
            {
                if (directFilters.Count > 0 || cont > 0)
                {
                    string concatenator = childFilter.Concatenate ?? "AND";
                    query.Append("  " + concatenator + " ");
                }

                var propertyMap = EntityProperties.FirstOrDefault(p => childFilter.PropertyName.Contains(p.PropertyName));
                var childMaps = MapLocator.GetMap(propertyMap.PropertyType);

                if (propertyMap.MappingType == MappedProperty.MappingTypes.HasMany)
                {
                    var childParentReference = childMaps.FirstOrDefault(x => x.PropertyType == typeof (T));

                    switch(childFilter.Comparison)
                    {
                        case FilterOptions.In:
                            query.AppendLine(EntityName + ".ID IN ");
                            query.AppendLine("(SELECT " + childParentReference.PropertyName + "ID FROM ");
                            query.AppendLine(propertyMap.PropertyType.Name + " WHERE ID IN (" + childFilter.CompareValue +"))");
                            break;
                        case FilterOptions.NotIn:
                            query.AppendLine(EntityName + ".ID NOT IN ");
                            query.AppendLine("(SELECT " + childParentReference.PropertyName + "ID FROM ");
                            query.AppendLine(propertyMap.PropertyType.Name + " WHERE ID IN (" + childFilter.CompareValue + "))");
                            break;
                        case FilterOptions.IsNull:
                            query.AppendLine("(SELECT COUNT(0) FROM " + propertyMap.PropertyType.Name);
                            query.AppendLine(" WHERE " + childParentReference.PropertyName + "ID = " + EntityName + ".ID) = 0");
                            break;
                        case FilterOptions.IsNotNull:
                            query.AppendLine("(SELECT COUNT(0) FROM " + propertyMap.PropertyType.Name);
                            query.AppendLine(" WHERE " + childParentReference.PropertyName + "ID = " + EntityName + ".ID) > 0");
                            break;
                        default:
                            throw new NotSupportedException(childFilter.Comparison.ToString());
                    }
                }

                else if (propertyMap.MappingType == MappedProperty.MappingTypes.HasManyToMany)
                {
                    string manyToManyTableName = MappedProperty.GetManyToManyTableName(typeof(T).Name, propertyMap.PropertyType.Name);

                    switch (childFilter.Comparison)
                    {
                        case FilterOptions.In:
                            query.AppendLine(EntityName + ".ID IN ");
                            query.AppendLine("(SELECT " + EntityName + "ID FROM ");
                            query.AppendLine(manyToManyTableName + " WHERE " + propertyMap.PropertyType.Name + "ID IN (" + childFilter.CompareValue + "))");
                            break;
                        case FilterOptions.NotIn:
                            query.AppendLine(EntityName + ".ID NOT IN ");
                            query.AppendLine("(SELECT " + EntityName + "ID FROM ");
                            query.AppendLine(manyToManyTableName + " WHERE " + propertyMap.PropertyType.Name + "ID IN (" + childFilter.CompareValue + "))");
                            break;
                        case FilterOptions.IsNull:
                            query.AppendLine("(SELECT COUNT(0) FROM " + manyToManyTableName);
                            query.AppendLine(" WHERE " + EntityName + "ID = " + EntityName + ".ID) = 0");
                            break;
                        case FilterOptions.IsNotNull:
                            query.AppendLine("(SELECT COUNT(0) FROM " + manyToManyTableName);
                            query.AppendLine(" WHERE " + EntityName + "ID = " + EntityName + ".ID) > 0");
                            break;
                        default:
                            throw new NotSupportedException(childFilter.Comparison.ToString());
                    }
                }

                cont++;
            }
        }

        private void AddDirectFilters(StringBuilder query, List<FilterExpression> directFilters)
        {
            if (directFilters.Count > 0)
                query.AppendLine(directFilters[0].ToString());

            if (directFilters.Count > 1)
            {
                for (int f = 1; f < directFilters.Count; f++)
                {
                    var concatenator = directFilters[f].Concatenate ?? "AND";
                    query.Append("  " + concatenator + " ");

                    query.AppendLine(directFilters[f].ToString());
                }
            }
        }

        private void AddWhere(StringBuilder query)
        {
            if (Filters == null)
                query.AppendLine("WHERE ");
            else
                query.AppendLine("AND");
            
            query.AppendLine(WhereClause);
        }

        private void AddOrderByClause(StringBuilder query)
        {
            query.AppendLine("ORDER BY");
            query.AppendLine(OrderByClause);
        }

        private void AddOrders(StringBuilder query)
        {
            query.AppendLine("ORDER BY");

            query.Append("      ");

            if (PropertiesToJoin != null)
                query.Append(EntityName + ".");

            query.AppendLine(Orders[0].ToString());

            if (Orders.Count > 1)
            {
                for (int f = 1; f < Orders.Count; f++)
                {
                    query.Append("     ,");

                    if (!Orders[f].ToString().Contains("."))
                        query.Append(EntityName + ".");

                    query.AppendLine(Orders[f].ToString());
                }
            }
        }
    }
}
